//
// PepResolverImpl
//
// Copyright (C) 2007 Brian Stevens.
//
//  This file is part of the JavaPepper Library.
//
//  The JavaPepper Library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The JavaPepper Library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the JavaPepper Library; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.javapepper;

import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import javax.servlet.http.*;
import net.jextra.javapepper.PepSession.*;

/**
 * <p>
 * This is the default PepResolver used if no overriding resolver is specified in the configuration file.
 * </p>
 * <p>
 * <ol>
 * <li>It first searches for a "local" file on the server and if it exists, returns that file.</li>
 * <li>If that fails, if there is a "local" directory, then just return the "welcome" page.</li>
 * <li>If that fails, get the proper {@link PepResponder} from the {@link PepSession}.</li>
 * </ol>
 * </p>
 */
public class PepResolverImpl implements PepResolver
{
    // ============================================================
    // Fields
    // ============================================================

    public static final String METHOD_PUT = "PUT";
    public static final String METHOD_POST = "POST";
    public static final String METHOD_GET = "GET";
    public static final String METHOD_DELETE = "DELETE";

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    /**
     * This is very important to be synchronized. This is because of responders are not required to be thread-safe and
     * therefore need to run one-after-the-other.
     */
    @Override
    public void processRequest( PepSession pepSession, HttpServletRequest request, HttpServletResponse response )
        throws Exception
    {
        //
        // See if there is a file on server. If the request corresponds to an actual File that
        // exists, process the file directly.
        //
        File file = getServerFile( request );
        if ( file.exists() && file.isFile() )
        {
            processServerFileRequest( request, response );
            return;
        }

        //
        // Default to welcome URL if directory with specified path exists.
        //
        if ( pepSession.getWelcomeURL() != null && file.isDirectory() )
        {
            String url = request.getContextPath();
            if ( !pepSession.getWelcomeURL().startsWith( "/" ) )
            {
                url += "/";
            }
            url += pepSession.getWelcomeURL();
            response.sendRedirect( url );
            return;
        }

        //
        // See if there is a mapping for this URL. If so, just stream the mapped file back to the
        // client.
        //
        String mappingUrl = getRelativeURL( request );
        Mapping mapping = pepSession.matchMapping( mappingUrl );
        if ( mapping != null )
        {
            file = new File( request.getSession().getServletContext().getRealPath( mapping.getPath() ) );
            if ( file.exists() )
            {
                streamFile( file, request, response );
                return;
            }
            else
            {
                pepSession.setLastErrorCode( HttpServletResponse.SC_NOT_FOUND );
                throw new RuntimeException( "Mapped path for URL " + mappingUrl + " not found." );
            }
        }

        //
        // Create new responder registered to handle the requested URL path.
        //
        String url = getRelativeURL( request );
        PepResponder responder = pepSession.createResponder( url );

        PepMethod requestMethod = PepMethod.valueOf( request.getMethod() );
        forward( responder, pepSession, requestMethod, request, response );
    }

    @Override
    public void forward( PepResponder responder, PepSession pepSession, PepMethod method, HttpServletRequest request,
        HttpServletResponse response )
        throws Exception
    {
        //
        // Read the variables from the session.
        //
        readSessionVariables( pepSession, responder );

        //
        // Call the responder.
        //
        //        responder.processRequest( pepSession, method, request, response );

        // @note
        // Do not flush the buffer. The servlet container will flush the buffer. If you flush
        // the buffer, it can cause IO Exceptions.
        //pepSession.getHttpResponse().flushBuffer();

        //
        // Write the variables to the session.
        //
        writeSessionVariables( pepSession, responder );
    }

    public void streamFile( File file, HttpServletRequest request, HttpServletResponse response )
        throws IOException
    {
        response.setContentType( request.getSession().getServletContext().getMimeType( request.getRequestURI() ) );

        @SuppressWarnings( "resource" )
        FileInputStream in = new FileInputStream( file );
        byte[] buffer = new byte[10000];
        for ( int len = in.read( buffer ); len > 0; len = in.read( buffer ) )
        {
            try
            {
                response.getOutputStream().write( buffer, 0, len );
            }
            catch ( IOException ex )
            {
                // Ignore client aborted exceptions. Note, search Class name so not dependent
                // on any particular web server.
                if ( ex.getClass().getName().contains( "ClientAbort" ) )
                {
                    break;
                }
                else
                {
                    // Rethrow.
                    throw ex;
                }
            }
        }
        in.close();
    }

    /**
     * Read the server-side file and pass it through as-is. This is an "override" to the default behaviour of looking up
     * the {@link PepResponder}.
     */
    public void processServerFileRequest( HttpServletRequest request, HttpServletResponse response )
        throws IOException
    {
        // Assume if a file that it is simply static and let it cache for an hour.
        response.setHeader( "Cache-Control", "max-age=3600" );

        File file = getServerFile( request );
        streamFile( file, request, response );
    }

    /**
     * Parse request and get corresponding server-side {@link File}.
     */
    public File getServerFile( HttpServletRequest request )
        throws UnsupportedEncodingException
    {
        File file = new File( request.getSession().getServletContext().getRealPath( getRelativeURL( request ) ) );
        return file;
    }

    public String getRelativeURL( HttpServletRequest request )
        throws UnsupportedEncodingException
    {
        String url = request.getServletPath();
        // Trim off starting / if there is one.
        if ( url.startsWith( "/" ) )
        {
            url = url.substring( 1 );
        }
        url = URLDecoder.decode( url, "UTF-8" );
        return url;
    }

    // ----------
    // protected
    // ----------

    /**
     * Read attributes from the HttpSession into corresponding annotated fields.
     */
    protected void readSessionVariables( PepSession pepSession, Object parent )
        throws Exception
    {
        // Walk the inheritance chain.
        for ( Class<?> cls = parent.getClass(); cls != null; cls = cls.getSuperclass() )
        {
            // Check each field.
            for ( Field field : cls.getDeclaredFields() )
            {
                if ( field.isAnnotationPresent( PepSessionVariable.class ) )
                {
                    PepSessionVariable ann = field.getAnnotation( PepSessionVariable.class );
                    String key = getSessionVariableKey( field );
                    Object value = pepSession.getHttpSession().getAttribute( key );

                    // If the value is null, potentially create a value automatically.
                    if ( value == null && ann.autoConstruct() )
                    {
                        int mods = field.getType().getModifiers();

                        if ( !Modifier.isPublic( mods ) )
                        {
                            throw new Exception( "Can not access a session variable of class " +
                                field.getType().getName() + " without the modifier \"public\"" );
                        }

                        if ( field.getType().isMemberClass() && !Modifier.isStatic( mods ) )
                        {
                            throw new Exception( "Unable to instantiate session variable which " +
                                "is an inner class " + field.getType().getName() + " and not \"static\": " );
                        }

                        value = field.getType().newInstance();
                        pepSession.getHttpSession().setAttribute( key, value );

                        // Recursively call readSessionVariables in order to fill out
                        // value.
                        readSessionVariables( pepSession, value );
                    }

                    field.setAccessible( true );
                    field.set( parent, value );
                }
            }
        }
    }

    /**
     * Write the fields of the "parent" back to HttpSession as attributes.
     */
    protected void writeSessionVariables( PepSession pepSession, Object parent )
        throws IllegalArgumentException, IllegalAccessException, SecurityException, InstantiationException,
        InvocationTargetException, NoSuchMethodException
    {
        // Walk the inheritance chain.
        for ( Class<?> cls = parent.getClass(); cls != null; cls = cls.getSuperclass() )
        {
            // Check each field.
            for ( Field field : cls.getDeclaredFields() )
            {
                if ( field.isAnnotationPresent( PepSessionVariable.class ) )
                {
                    String key = getSessionVariableKey( field );

                    field.setAccessible( true );
                    Object value = field.get( parent );
                    pepSession.getHttpSession().setAttribute( key, value );

                    // Recursively call writeSessionVariables in order to completely save
                    // any session variables in session variables.
                    if ( value != null )
                    {
                        writeSessionVariables( pepSession, value );
                    }
                }
            }
        }
    }

    // ----------
    // private
    // ----------

    private String getSessionVariableKey( Field field )
        throws InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException,
        InvocationTargetException, NoSuchMethodException
    {
        PepSessionVariable ann = field.getAnnotation( PepSessionVariable.class );
        String key = ann.value().trim();

        if ( key.isEmpty() )
        {
            key = field.getDeclaringClass().getName() + "$" + field.getName();
        }
        else
        {
            // Defined keys by users can not contain reserved "$" character.
            key = key.replace( '$', '_' );
        }

        return key;
    }
}
